package eu.darken.octi.sync.module.syncs.jserver.core

import com.gitee.wsl.exception.logTag
import com.squareup.moshi.Moshi
import eu.darken.octi.sync.module.core.ModuleId
import eu.darken.octi.sync.core.DeviceId
import eu.darken.octi.sync.core.SyncSettings
import eu.darken.octi.sync.core.encryption.PayloadEncryption
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okio.ByteString
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import java.time.Instant

class JServerEndpoint constructor(
    private val serverAdress: JServer.Address,
    private val dispatcherProvider: CoroutineDispatcher=Dispatchers.IO,
    private val syncSettings: SyncSettings,
    private val baseHttpClient: OkHttpClient,
    private val baseMoshi: Moshi,
    private val basicAuthInterceptor: BasicAuthInterceptor,
) {

    private val httpClient by lazy {
        baseHttpClient.newBuilder().apply {
            addInterceptor(basicAuthInterceptor)
        }.build()
    }

    private val api: JServerApi by lazy {
        Retrofit.Builder().apply {
            baseUrl(serverAdress.httpUrl)
            client(httpClient)
            addConverterFactory(MoshiConverterFactory.create(baseMoshi).asLenient())
        }.build().create(JServerApi::class.java)
    }

    private val ourDeviceIdString: String
        get() = syncSettings.deviceId.id

    private var credentials: JServer.Credentials? = null
    fun setCredentials(credentials: JServer.Credentials?) {
        Timber.d( "setCredentials(credentials=$credentials)" )
        basicAuthInterceptor.setCredentials(credentials)
        this.credentials = credentials
    }

    suspend fun createNewAccount(): JServer.Credentials = withContext(dispatcherProvider) {
        Timber.d( "createNewAccount()" )
        val response = api.register(deviceID = ourDeviceIdString)

        JServer.Credentials(
            createdAt = Instant.now(),
            serverAdress = serverAdress,
            accountId = JServer.Credentials.AccountId(response.accountID),
            devicePassword = JServer.Credentials.DevicePassword(response.password),
            encryptionKeyset = PayloadEncryption().exportKeyset()
        )
    }

    data class LinkedAccount(
        val accountId: JServer.Credentials.AccountId,
        val devicePassword: JServer.Credentials.DevicePassword,
    )

    suspend fun linkToExistingAccount(
        linkCode: JServer.Credentials.LinkCode,
    ): LinkedAccount = withContext(dispatcherProvider) {
        Timber.d( "linkToExistingAccount(linkCode=$linkCode)" )
        val response = api.register(
            deviceID = ourDeviceIdString,
            shareCode = linkCode.code,
        )

        LinkedAccount(
            accountId = JServer.Credentials.AccountId(response.accountID),
            devicePassword = JServer.Credentials.DevicePassword(response.password),
        )
    }

    suspend fun createLinkCode(): JServer.Credentials.LinkCode = withContext(dispatcherProvider) {
        Timber.d( "createLinkCode(account=$credentials)")
        val response = api.createShareCode(deviceID = ourDeviceIdString)
        return@withContext JServer.Credentials.LinkCode(code = response.shareCode)
    }

    suspend fun listDevices(linkCode: JServer.Credentials.LinkCode? = null): Collection<DeviceId> =
        withContext(dispatcherProvider) {
            Timber.d( "listDevices(linkCode=$linkCode)")
            val response = api.getDeviceList(
                deviceID = ourDeviceIdString,
            )
            response.items.map { DeviceId(it.id) }
        }

    data class ReadData(
        val modifiedAt: Instant,
        val payload: ByteString,
    )

    suspend fun readModule(deviceId: DeviceId, moduleId: ModuleId): ReadData? = withContext(dispatcherProvider) {
        Timber.d( "readModule(deviceId=$deviceId, moduleId=$moduleId)")
        val response = api.readModule(
            callerDeviceId = ourDeviceIdString,
            moduleId = moduleId.id,
            targetDeviceId = deviceId.id.takeIf { it != ourDeviceIdString }
        )

        if (!response.isSuccessful) throw HttpException(response)

        val lastModifiedAt = response.headers()["X-Modified-At"]?.let { Instant.parse(it) } ?: return@withContext null

        val body = response.body()?.byteString()?.takeIf { it.toString() != NULL_BODY } ?: ByteString.EMPTY

        ReadData(
            modifiedAt = lastModifiedAt,
            payload = body,
        )
    }

    suspend fun writeModule(moduleId: ModuleId, payload: ByteString) = withContext(dispatcherProvider) {
        Timber.d( "writeModule(moduleId=$moduleId, payload=$payload)")
        api.writeModule(
            deviceId = ourDeviceIdString,
            moduleId = moduleId.id,
            payload = payload.toRequestBody(),
        )
    }

    suspend fun deleteModules(deviceId: DeviceId) = withContext(dispatcherProvider) {
        api.deleteModules(
            callerDeviceId = ourDeviceIdString,
            targetDeviceId = deviceId.id.takeIf { it != ourDeviceIdString }
        )
    }

    suspend fun getHealth(): JServerApi.Health = withContext(dispatcherProvider) {
        Timber.d( "getHealth()")
        api.getHealth()
    }


    interface Factory {
        fun create(account: JServer.Address): JServerEndpoint
    }

    companion object {
        private val NULL_BODY = "null"
        private val TAG = logTag("Sync", "JServer", "Connector", "Endpoint")
    }
}

